
import { AnyHolder } from "@/utils/AnyHolder";
import { RTCSocket } from ".";
import { IMember } from "./IMember";
import { IMemberInfo } from "./IMemberInfo";
import { IMemberViewDelegate } from "./IMemberViewDelegate";
import { INFO, INFO_ARGS, WARN } from "./utils/annotation/INFO";

@INFO_ARGS
export class Member extends IMember {

  protected _socket: RTCSocket | null = null;
  protected _outbound_stream: MediaStream | null = null;
  protected _info: IMemberInfo;
  protected _clean_up: ((() => void) | undefined)[];
  protected _view: IMemberViewDelegate | null = null;
  protected _polite: boolean = false;
  protected _is_local: boolean = false;
  protected _viewing_stream: MediaStream | null = null;

  protected _inbound_stream: MediaStream | null = null;
  protected _peer_conn: RTCPeerConnection | null = null;

  get info() { return this._info; }
  set info(v) { this._info = v; }
  get username() { return this._info.username; }
  get polite() { return this._polite; }
  get element() { return this._view; }
  get is_local() { return this._is_local }
  constructor(
    info: IMemberInfo,
    outbound_stream: AnyHolder<MediaStream>,
    socket?: AnyHolder<RTCSocket>,
  ) {
    super();
    this._is_local = !socket;
    this._info = info;
    this._clean_up = [
      outbound_stream.on('create', ev => this._set_outbound_stream(ev.detail)),
      outbound_stream.on('clear', ev => this._set_outbound_stream(ev.detail)),
      socket?.on('clear', ev => this._set_socket(ev.detail)),
      socket?.on('create', ev => this._set_socket(ev.detail)),
    ];
    if (socket) this._set_socket(socket.direct);
    this._set_outbound_stream(outbound_stream.direct);
  }

  destructor() {
    this._set_outbound_stream(null);
    this._set_socket(null);
    for (const func of this._clean_up) func && func();
    if (this._view) this._view.element.srcObject = null;
  }

  set_polite(v: boolean): this {
    this._polite = v;
    return this;
  }

  @INFO
  add_track(track: MediaStreamTrack): this {
    const pc = this._peer_conn;
    if (!pc) {
      console.log('add_track, but peer connection not created'); return this;
    }
    this._track_senders.set(track.id, pc.addTrack(track));
    return this;
  }

  @INFO
  remove_track(track: MediaStreamTrack): this {
    const tid = track.id;
    const track_sender = this._track_senders.get(tid);
    if (!track_sender) {
      console.log('remove_track, but track not found');
      return this;
    }
    const pc = this._peer_conn;
    this._track_senders.delete(tid);
    if (!pc) {
      console.log('remove_track, but peer connection not created');
      return this;
    }
    pc.removeTrack(track_sender);
    return this;
  }

  get socket(): RTCSocket | null { return this._socket; }

  @INFO
  protected _set_socket(v: RTCSocket | null) {
    if (this._socket === v) return;
    this._socket
      ?.off("ice_candidate", this.cb_handle_remote_ice_candidate)
      .off("negotiation", this.cb_handle_remote_session_desc);
    this._socket = v;
    v?.on("negotiation", this.cb_handle_remote_session_desc)
      .on("ice_candidate", this.cb_handle_remote_ice_candidate);
    this._destory_pc();
    if (v) this._create_pc();
    if (v) this._connect_tracks();
  }

  @INFO
  protected _set_outbound_stream(v: MediaStream | null) {
    if (this._outbound_stream === v) return;
    if (!v) this._clear_track_senders();
    this._outbound_stream = v;
    if (v) this._connect_tracks();

    if (this._is_local) this._bind_outbound_stream()

  }

  protected _track_senders = new Map<string, RTCRtpSender>();

  @INFO
  protected _clear_track_senders() {
    const pc = this._peer_conn;
    if (!pc) return;
    this._track_senders.forEach((v) => pc.removeTrack(v));
    this._track_senders.clear();
  }

  @INFO
  protected _connect_tracks() {
    const st = this._outbound_stream;
    if (!st) { WARN.print('[Member._connect_tracks]', 'failed. _outbound_stream not set '); return; }

    const pc = this._peer_conn;
    if (!pc) { WARN.print('[Member._connect_tracks]', 'failed. _peer_conn not set '); return; }

    const tracks = st.getTracks();
    if (!tracks.length) { WARN.print(`[Member._connect_tracks]`, 'length of tracks is 0'); return; }
    else { INFO.print(`[Member._connect_tracks]`, `length of tracks is ${tracks.length}`); }

    for (const track of tracks) {
      this.add_track(track);
    }
  }

  @INFO
  protected _create_pc() {
    const pc = this._peer_conn = new RTCPeerConnection({
      iceServers: [{ 'urls': ['stun:stun.gim.ink:3478'] }]
    });
    pc.onicecandidate = (e) => this._pc_on_ice_candidate(e);
    pc.onnegotiationneeded = (e) => this._pc_on_negotiation_needed(e);
    pc.oniceconnectionstatechange = (e) => this._pc_on_ice_connection_state_change(e);
    pc.ontrack = (e) => this._pc_on_track(e);
    pc.onicecandidateerror = (e) => this._pc_on_ice_candidate_error(e);
    pc.onicegatheringstatechange = (e) => this._pc_on_icegathering_state_change(e);
    pc.onsignalingstatechange = (e) => this._pc_on_signaling_state_change(e);
    pc.onconnectionstatechange = (e) => this._pc_on_connection_state_change(e);
  }

  @INFO
  protected _destory_pc() {
    this._peer_conn?.close();
    this._peer_conn = null;
  }

  protected _making_offer = false;
  protected _ignore_offer = false;

  @INFO
  protected _pc_on_ice_candidate({ candidate }: RTCPeerConnectionIceEvent) {
    if (!candidate) return;
    this._socket?.emit('ice_candidate', this._info, candidate, () => { });
  }

  @INFO
  protected async _pc_on_negotiation_needed(ev: Event) {
    try {
      this._making_offer = true;
      const pc = ev.target as RTCPeerConnection;
      await pc.setLocalDescription();
      this._socket?.emit('negotiation', this._info, pc.localDescription!, () => { });
    } catch (e) {
      console.error(e);
    } finally {
      this._making_offer = false;
    }
  }

  @INFO
  protected _pc_on_ice_connection_state_change(ev: Event) {
    const pc = ev.target as RTCPeerConnection;
    if (pc.iceConnectionState === "failed") {
      pc.restartIce();
    }
  }

  @INFO
  protected _pc_on_ice_candidate_error(e: Event): any {
  }

  @INFO
  protected _pc_on_icegathering_state_change(e: Event): any {
  }

  @INFO
  protected _pc_on_signaling_state_change(e: Event): any {
  }

  @INFO
  protected _pc_on_connection_state_change(e: Event): any {
  }

  @INFO
  protected async _handle_remote_session_desc(desc: RTCSessionDescriptionInit) {
    const pc = this._peer_conn!;

    const offer_collision = desc.type === "offer" && (this._making_offer || pc.signalingState !== "stable");
    this._ignore_offer = offer_collision && !this.polite;
    if (this._ignore_offer) return;

    pc.setRemoteDescription(desc);
    if (desc.type === 'offer') {
      await pc.setLocalDescription();
      this._socket?.emit('negotiation', this._info, pc.localDescription!, () => { });
    }
  }

  protected cb_handle_remote_session_desc = this._handle_remote_session_desc.bind(this);

  protected async _handle_remote_ice_candidate(candidate: RTCIceCandidate) {
    const pc = this._peer_conn!;

    try {
      await pc.addIceCandidate(candidate);
    } catch (err: any) {
      if (!this._ignore_offer) {
        throw err;
      }
    }
  }

  protected cb_handle_remote_ice_candidate = this._handle_remote_ice_candidate.bind(this);

  @INFO_ARGS
  protected _bind_outbound_stream() {
    this.set_viewing_stream(this._outbound_stream);
  }

  @INFO_ARGS
  protected _bind_inbound_stream() {
    this.set_viewing_stream(this._inbound_stream);
  }

  @INFO_ARGS
  set_viewing_stream(stream: MediaStream | null) {
    this._viewing_stream = stream;
    const element = this._view?.element;
    if (!element) return;
    if (element.srcObject !== this._viewing_stream)
      element.srcObject = this._viewing_stream;
    if (!this._inbound_stream?.getVideoTracks().length)
      element.load()
  }

  @INFO_ARGS
  protected _on_pc_track_mute(track: MediaStreamTrack) {
    if (!this._inbound_stream) return;
    this._inbound_stream.removeTrack(track);
    if (this._inbound_stream.getTracks().length === 0)
      this._inbound_stream = null;
    if (!this._inbound_stream)
      this._bind_inbound_stream();
  }

  @INFO_ARGS
  protected _on_pc_track_unmute(track: MediaStreamTrack) {
    const prev_inbound_stream = this._inbound_stream
    const inbound_stream = this._inbound_stream ? this._inbound_stream : this._inbound_stream = new MediaStream()
    inbound_stream.addTrack(track);
    if (prev_inbound_stream === null)
      this._bind_inbound_stream();
  }

  @INFO_ARGS
  protected _pc_on_track({ track }: RTCTrackEvent) {
    track.addEventListener('mute', () => this._on_pc_track_mute(track))
    track.addEventListener('unmute', () => this._on_pc_track_unmute(track))
    track.addEventListener('ended', () => this._on_pc_track_mute(track))
  }

  @INFO_ARGS
  set_view(element: IMemberViewDelegate | null) {
    if (this._view === element)
      return this;
    this._view = element;
    if (this._is_local)
      this._bind_outbound_stream()
    else
      this._bind_inbound_stream()
    return this;
  }
}